/*
 * This file is part of LiquidBounce (https://github.com/CCBlueX/LiquidBounce)
 *
 * Copyright (c) 2015 - 2025 CCBlueX
 *
 * LiquidBounce is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * LiquidBounce is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with LiquidBounce. If not, see <https://www.gnu.org/licenses/>.
 */
package net.ccbluex.liquidbounce.features.module.modules.exploit

import net.ccbluex.liquidbounce.config.types.nesting.Choice
import net.ccbluex.liquidbounce.config.types.nesting.ChoiceConfigurable
import net.ccbluex.liquidbounce.event.waitTicks
import net.ccbluex.liquidbounce.event.events.NotificationEvent
import net.ccbluex.liquidbounce.event.events.OverlayRenderEvent
import net.ccbluex.liquidbounce.event.handler
import net.ccbluex.liquidbounce.event.tickHandler
import net.ccbluex.liquidbounce.features.module.Category
import net.ccbluex.liquidbounce.features.module.ClientModule
import net.ccbluex.liquidbounce.features.module.modules.movement.fly.ModuleFly
import net.ccbluex.liquidbounce.render.engine.type.Color4b
import net.ccbluex.liquidbounce.utils.block.canStandOn
import net.ccbluex.liquidbounce.utils.block.getState
import net.ccbluex.liquidbounce.utils.client.notification
import net.ccbluex.liquidbounce.utils.client.scaledDimension
import net.ccbluex.liquidbounce.utils.client.toRadians
import net.ccbluex.liquidbounce.utils.collection.Pools
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction
import kotlin.math.cos
import kotlin.math.sin

/**
 * Clip module
 *
 * Allows you to clip through blocks by jumping or snekaing.
 */
object ModuleClip : ClientModule("Clip", Category.MOVEMENT) {

    val modes = choices("Choice", Fancy, arrayOf(Fancy, Old))

    object Old : Choice("Old") {

        override val parent: ChoiceConfigurable<Choice>
            get() = modes

        private val horizontal by float("Horizontal", 0f, -10f..10f)
        private val vertical by float("Vertical", 5f, -10f..10f)
        private val resetVelocity by boolean("ResetVelocity", true)

        override fun enable() {
            val yaw = player.yaw.toRadians()
            val x = -sin(yaw) * horizontal
            val z = cos(yaw) * horizontal

            player.updatePosition(player.x + x, player.y + vertical, player.z + z)
            if (resetVelocity) {
                player.setVelocity(0.0, 0.0, 0.0)
            }
            enabled = false
        }
    }

    object Fancy : Choice("Fancy") {

        override val parent: ChoiceConfigurable<Choice>
            get() = modes


        private val horizontal by int("Horizontal", 0, 0..6)
        private val vertical by int("Vertical", 5, 0..6)

        private val requiresStandOn by boolean("RequiresStandOn", true)

        private val possibleClipDirections = hashSetOf<Direction>()

        @Suppress("unused")
        val repeatable = tickHandler {
            possibleClipDirections.clear()

            if (ModuleFly.running) {
                return@tickHandler
            }

            arrayOf(Direction.UP, Direction.DOWN).forEach { direction ->
                tryClip(direction, vertical) {
                    possibleClipDirections.add(direction)
                }
            }

            val movementDirection = when {
                player.horizontalCollision -> when {
                    mc.options.forwardKey.isPressed -> player.horizontalFacing
                    mc.options.backKey.isPressed -> player.horizontalFacing.opposite
                    mc.options.leftKey.isPressed -> player.horizontalFacing.rotateClockwise(Direction.Axis.Y).opposite
                    mc.options.rightKey.isPressed -> player.horizontalFacing.rotateClockwise(Direction.Axis.Y)
                    else -> return@tickHandler
                }

                mc.options.sneakKey.isPressed -> Direction.DOWN
                mc.options.jumpKey.isPressed -> Direction.UP
                else -> return@tickHandler
            }

            val clipLength = when (movementDirection) {
                Direction.DOWN, Direction.UP -> vertical
                else -> horizontal
            }

            tryClip(movementDirection, clipLength) { blockPos ->
                // Set the new position
                player.updatePosition(blockPos.x + 0.5, blockPos.y.toDouble(), blockPos.z + 0.5)

                notification("Clip", message("whoosh"), NotificationEvent.Severity.SUCCESS)
            }

            waitTicks(5)
        }

        @Suppress("unused")
        val overlayRenderHandler = handler<OverlayRenderEvent> { event ->
            if (possibleClipDirections.isEmpty()) {
                return@handler
            }

            val directionString = Pools.buildStringPooled {
                append("[ ")
                if (possibleClipDirections.contains(Direction.UP)) {
                    append('▲')
                }
                if (possibleClipDirections.contains(Direction.DOWN)) {
                    append('▼')
                }
                append(" ]")
            }

            // Draw it to the right of the crosshair
            val (width, height) = mc.window.scaledDimension

            // Since we cannot use the LiquidBounce FontRenderer for characters like ▲ and ▼
            // we have to use the Minecraft FontRenderer.
            // Another option would be to pass the text to the JCEF HUD and render it there.
            // But that would be a lot of work for a simple arrow.
            // todo: maybe add a font renderer that supports unicode characters?
            //   or a way to pass text to the JCEF HUD
            event.context.drawTextWithShadow(
                mc.textRenderer,
                directionString,
                width / 2 + 10,
                height / 2 - mc.textRenderer.fontHeight / 2 + 1,
                Color4b.WHITE.toARGB()
            )
        }

        private inline fun tryClip(movementDirection: Direction, length: Int, clip: (BlockPos) -> Unit) {
            if (length == 0) {
                return
            }

            var wallBetween = false

            // Calculate the new position
            // Find the closest free block to clip into
            val position = player.blockPos.mutableCopy()
            repeat(length) {
                position.move(movementDirection)

                if (isPossibleLocation(position, requiresStandOn = requiresStandOn &&
                        movementDirection != Direction.UP)) {
                    // We do not want to clip if there is no wall between us
                    if (wallBetween) {
                        clip(position)
                        return
                    }
                } else {
                    wallBetween = true
                }
            }
        }

        private fun isPossibleLocation(blockPos: BlockPos, requiresStandOn: Boolean = true): Boolean {
            if (requiresStandOn && !blockPos.down().canStandOn()) {
                return false
            }

            return blockPos.getState()?.isAir == true && blockPos.up().getState()?.isAir == true
        }
    }

}
